<?php

/**
 * @copyright Nichlas Severinsen
 * @license   https://opensource.org/licenses/BSD-2-Clause
 */

namespace Necklace\XxHash\Xxh3;

use Exception;
use FFI;
use Necklace\XxHash\XxHash;
use Necklace\XxHash\XxHashErrorCode;

final class XxHash64 extends XxHash
{
	/**
	 * Constructor.
	 */
	public function __construct()
    {
		parent::__construct();

		if($this->version < 701)
		{
			throw new Exception('Too old version of libxxhash. XXH3 requires v0.7.1 or above.');
		}

		$this->ffi = FFI::load(__DIR__ . '/../../include/xxh3/xxhash64-php.h');

		$this->state = $this->ffi->XXH3_createState();
	}

	/**
	 * Destructor.
	 */
	public function __destruct()
	{
		if($this->ffi->XXH3_freeState($this->state) !== XxHashErrorCode::OK)
		{
			throw new Exception('XxHash unable to free state.');
		}
	}

	/**
	 * Similar to the native XXH64, but returns the hash as a string instead.
	 *
	 * Notice: the xxhash documentation recommends using this function for small inputs only.
	 *
	 * @param  string $data   Data you want to hash.
	 * @param  int    $seed   Seed used to alter the result.
	 * @param  string $secret Secret used to alter the result.
	 * @return string
	 */
	public function hash(string $data, int $seed = null, string $secret = null): string
	{
		if($seed)
		{
			$hash = $this->ffi->XXH3_64bits_withSeed($data, strlen($data), $seed);
		}

		if($secret)
		{
			if(strlen($secret) < Xxh3::XXH3_SECRET_SIZE_MIN)
			{
				throw new Exception('XxHash XXH3 secret is too small.');
			}

			$hash = $this->ffi->XXH3_64bits_withSecret($data, strlen($data), $secret, strlen($secret));
		}

		if(!$seed && !$secret)
		{
			$hash = $this->ffi->XXH3_64bits($data, strlen($data));
		}

		if(!$hash)
		{
			throw new Exception('XxHash unable to hash given data.');
		}

		$canonical = $this->ffi->new('XXH64_canonical_t');

		$this->ffi->XXH64_canonicalFromHash(FFI::addr($canonical), $hash);

		return $this->canonicalToString($canonical);
	}

	/**
	 * Starts a new hash by initializing state (with or without seed).
	 *
	 * @param  int    $seed   Seed used to alter the result.
	 * @param  string $secret Secret used to alter the result.
	 * @return bool
	 */
	public function reset(int $seed = null, string $secret = null): bool
	{
		$statusCode = null;

		if($seed)
		{
			$statusCode = $this->ffi->XXH3_64bits_reset_withSeed($this->state, $seed);
		}

		if($secret)
		{
			if(strlen($secret) < Xxh3::XXH3_SECRET_SIZE_MIN)
			{
				throw new Exception('XxHash XXH3 secret is too small.');
			}

			$statusCode = $this->ffi->XXH3_64bits_reset_withSecret($this->state, $secret, strlen($secret));
		}

		if(!$seed && !$secret)
		{
			$statusCode = $this->ffi->XXH3_64bits_reset($this->state);
		}

		if($statusCode !== XxHashErrorCode::OK)
		{
			throw new Exception('XxHash unable to reset state.');
		}

		return true;
	}

	/**
	 * Feed the hash state with data.
	 *
	 * @param  string $data Feeding data.
	 * @return bool
	 */
	public function update(string $data): bool
	{
		if($this->ffi->XXH3_64bits_update($this->state, $data, strlen($data)) !== XxHashErrorCode::OK)
		{
			throw new Exception('XxHash unable to update state.');
		}

		return true;
	}

	/**
	 * Produce a hash from the fed data.
	 *
	 * @return string
	 */
	public function digest(): string
	{
		$hash = $this->ffi->XXH3_64bits_digest($this->state);

		$canonical = $this->ffi->new('XXH64_canonical_t');

		$this->ffi->XXH64_canonicalFromHash(FFI::addr($canonical), $hash);

		return $this->canonicalToString($canonical);
	}
}
